-
Notifications
You must be signed in to change notification settings - Fork 30
deleteMessage, prepareMessage noSend, streamMessageDeletions (libxmtp 1.9.0 updates) #765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+3,425
−259
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
|
android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt
Show resolved
Hide resolved
.../src/main/java/expo/modules/xmtpreactnativesdk/wrappers/EnrichedMessageQueryParamsWrapper.kt
Show resolved
Hide resolved
.../src/main/java/expo/modules/xmtpreactnativesdk/wrappers/EnrichedMessageQueryParamsWrapper.kt
Show resolved
Hide resolved
.../src/main/java/expo/modules/xmtpreactnativesdk/wrappers/EnrichedMessageQueryParamsWrapper.kt
Show resolved
Hide resolved
android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJsonV2.kt
Show resolved
Hide resolved
android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJsonV2.kt
Show resolved
Hide resolved
…ability when using conversations
neekolas
reviewed
Jan 29, 2026
neekolas
reviewed
Jan 29, 2026
neekolas
reviewed
Jan 29, 2026
neekolas
approved these changes
Jan 30, 2026
android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJsonV2.kt
Show resolved
Hide resolved
android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt
Outdated
Show resolved
Hide resolved
android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt
Show resolved
Hide resolved
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Add
Client.sendSyncRequest(), extend message preparation withnoSend, implementConversations.streamMessageDeletions(), and upgrade native SDKs to libxmtp 1.9.0 across iOS (XMTP 4.9.0) and Android (org.xmtp:android 4.9.0)Introduce manual device sync via
Client.sendSyncRequest(), support deferred publishing withConversation.prepareMessage(..., noSend), add real-time deletion streaming withConversations.streamMessageDeletions(), implement enriched message decoding/serialization (DecodedMessageV2) and deletion/leave-request codecs, expose publish/delete/enriched messages APIs, and passappDatathrough group creation; bump native dependencies to XMTP 4.9.0.📍Where to Start
Start with the deletion streaming flow at
Conversations.streamMessageDeletionsin Conversations.ts, then review the platform handlers in XMTPModule.swift and XMTPModule.kt.Changes since #765 opened
DecodedMessageV2.from()factory method to assign the recursively parsed reactions array or an empty array instead of passing through the decoded reactions directly [68233db]DecodedMessageV2.content()andDecodedMessageV2.decodedContent()methods to document FFI limitations withenrichedMessages()for unknown or custom content types and adjusted codec handling [68233db]enrichedMessages()does not applyJSContentCodec.decode()transformations [68233db]ContentJsonV2.toJsonMapin iOS to serialize unknown and custom content types as structured JSON [a789e15]nativeContent[a789e15]DecodedMessageUnionV2import and removed default initialization ofdeliveryStatusinDecodedMessageV2[a789e15]MessageWrapper.encodeToObjV2to conditionally include optional fields in result dictionary [e13af0a]uploadDebugInformationexported function, theXMTPDebugInformation.uploadDebugInformationinstance method, and the associated test case that verified upload key generation [df5dc25]updateAppDataPolicy: 'allow'field tocustomPermissionsPolicySetobjects in group permissions tests [2e00272]reactionV2content parsing inContentJsonwrapper to useReactionpayload type instead ofFfiReactionPayload, changed helper functions fromgetReactionV2ActionandgetReactionV2SchematogetReactionActionandgetReactionSchema, and populatedreferenceInboxIdfield from JSON data instead of using hardcoded empty string [cc2c241]FfiReactionPayloadconstruction withReactionconstruction in the reactionV2 branch ofContentJson.fromJsonObjparser [57d55a3]DeleteMessageCodec.contentType.typeIdfrom 'deletedMessage' to 'deleteMessage' [68d2712]ContentJsonwrapper to handle JSON null values forreferenceInboxId[6da1e24]📊 Macroscope summarized 7eed895. 30 files reviewed, 51 issues evaluated, 33 issues filtered, 1 comment posted
🗂️ Filtered Issues
android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt — 0 comments posted, 5 evaluated, 4 filtered
debugEventsEnabledparameter fromauthOptionsis no longer passed toClientOptions. The value is still parsed from JSON inauthParamsFromJsonand stored inAuthParamsWrapper, but the removal of this line means debug events configuration will be silently ignored. IfClientOptionsstill supports this parameter, users settingdebugEventsEnabled: truewill not get expected debug event behavior. [ Already posted ]sigRequest.lettosigRequest?.letinffiRevokeAllOtherInstallationsSignatureTextmeans the function will now silently returnnullifclient.ffiRevokeAllOtherInstallations()returns null. Callers expecting a non-null signature text string may encounter unexpected null values, potentially causingNullPointerExceptiondownstream. [ Low confidence ]Exceptionon line 2424 will also catchCancellationException, which can interfere with structured concurrency. When the coroutine is cancelled (e.g., viasubscriptions[...].cancel()), theCancellationExceptionwill be caught, logged as an error, and not rethrown. This prevents proper cancellation propagation and could cause the coroutine to not terminate cleanly. The catch block should either catch a more specific exception type or rethrowCancellationException. [ Already posted ]cancel()on the same job that is currently executing and failing. This is effectively a no-op since the job is already completing exceptionally, and represents dead code that suggests confusion about the coroutine lifecycle. [ Already posted ]android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt — 0 comments posted, 2 evaluated, 2 filtered
referenceInboxIdkey exists in the JSON but has anullvalue (e.g.,"referenceInboxId": null),reaction.get("referenceInboxId")will returnJsonNull(not Kotlinnull), so the safe call?.asStringwon't protect against it—callingasStringonJsonNullthrowsUnsupportedOperationException. Consider usingreaction.get("referenceInboxId")?.takeIf { !it.isJsonNull }?.asString ?: ""instead. [ Already posted ]fromJsonObjectmethod does not handle"leaveRequest"or"deleteMessage"JSON keys, buttoJsonMap()serializes them with those keys. If a caller serializes aLeaveRequestorDeleteMessageRequestviatoJsonMap()and then attempts to deserialize it viafromJson(), it will throw"Unknown content type"exception instead of round-tripping correctly. [ Already posted ]android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJsonV2.kt — 0 comments posted, 10 evaluated, 7 filtered
fromJsonObjectat line 143, thereactionV2branch creates aContentJsonwithFfiReactionPayloadas content type, butContentJson(shown in the diff) was updated to use the SDKReactiontype instead. The comment on line 147 inContentJsonexplicitly states "ReactionV2Codec encodes Reaction" - usingFfiReactionPayloadhere will cause a type mismatch when the codec tries to encode the content, likely resulting in aClassCastExceptionor encoding failure. [ Already posted ]ContentJsonV2.fromJsonObject, when parsingreactionV2, the code still createsFfiReactionPayload(line 143) and usesgetReactionV2Action/getReactionV2Schema(lines 145-146), but theReactionV2Codecwas updated to encodeReactiontype instead. The diff comment inContentJson.ktexplicitly states "Use SDK Reaction type (not FfiReactionPayload); ReactionV2Codec encodes Reaction." This inconsistency means messages created viaContentJsonV2.fromJsonwithreactionV2content will fail when encoded because the codec expectsReactionbut receivesFfiReactionPayload. [ Already posted ]ContentJsonV2.fromJsonObject(), thereactionV2handling creates aContentJsonwithFfiReactionPayloadtype at line 143, but the diff shows thatContentJson.ktwas updated to useReactiontype instead. TheReactionV2Codec(per the comment in the updatedContentJson.kt) encodesReaction, notFfiReactionPayload. This type mismatch will cause encoding failures or incorrect behavior when processing reactionV2 content throughContentJsonV2. [ Already posted ]fromJsonObject, thereactionV2branch creates aContentJsonwithFfiReactionPayloadas the content type (lines 142-151), but according to the changes inContentJson.kt,ReactionV2Codecnow expects the SDKReactiontype instead. This is an incomplete change -ContentJson.ktwas updated to useReactionwithgetReactionAction/getReactionSchema, butContentJsonV2.ktstill uses the oldFfiReactionPayloadwithgetReactionV2Action/getReactionV2Schema. This type mismatch will cause encoding failures when sending reactionV2 messages viaContentJsonV2.fromJsonObject. [ Already posted ]getReactionV2ActionandgetReactionV2Schemaare used which returnFfiReactionActionandFfiReactionSchematypes, but since the code constructsFfiReactionPayload(which requires these FFI types), if this is corrected to useReactiontype instead, these functions should be replaced withgetReactionActionandgetReactionSchemawhich return the SDK-compatible types. [ Already posted ]ContentJsonV2.fromJsonObject(), thereactionV2branch usesgetReactionV2Action()andgetReactionV2Schema()at lines 145-146, butContentJson.ktwas updated to usegetReactionAction()andgetReactionSchema()instead. These functions return different types (FfiReactionAction/FfiReactionSchemavs SDKReactionAction/ReactionSchema), causing type incompatibility with the expectedReactionconstructor parameters. [ Already posted ](content as? DeleteMessageRequest)?.messageIdwhencontentis already declared asDeleteMessageRequest?on line 318. This is unnecessary but harmless. [ Low confidence ]android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/EnrichedMessageQueryParamsWrapper.kt — 0 comments posted, 4 evaluated, 4 filtered
paramsJsoncontains malformed JSON that is not an object (e.g.,"hello",123, or[]), the.asJsonObjectcall on line 32 will throw anIllegalStateException. Consider wrapping this in a try-catch or validating the JSON structure. [ Already posted ]nullvalue (e.g.,{"limit": null}),jsonOptions.has("limit")returnstrue, but calling.asInt,.asLong, or.asStringon aJsonNullelement will throw anUnsupportedOperationException. This affects all field extractions (lines 36, 43, 50, 57, 72, 79, 86, 93). The code should check!jsonOptions.get("field").isJsonNullbefore calling the type conversion methods. [ Already posted ]excludeSenderInboxIdsexists in the JSON but is not an array (e.g., a string or object),getAsJsonArray("excludeSenderInboxIds")on line 64 will throw anIllegalStateException. [ Already posted ]excludeSenderInboxIdsarray containsnullelements in the JSON (e.g.,{"excludeSenderInboxIds": ["id1", null]}), theit.asStringcall on line 65 will throw anUnsupportedOperationExceptionwhen it encounters the null element. [ Already posted ]android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt — 0 comments posted, 1 evaluated, 1 filtered
createPermissionPolicySetFromJson, callingjsonObj.get("updateAppDataPolicy").asStringwill throw aNullPointerExceptionif the JSON input doesn't contain theupdateAppDataPolicyfield. This breaks backward compatibility with any existing JSON data that was created before this field was added. Theget()method returnsnullfor missing keys, and calling.asStringonnullcrashes. [ Already posted ]ios/Wrappers/CreateGroupParamsWrapper.swift — 0 comments posted, 1 evaluated, 1 filtered
as? Int64fordisappearStartingAtNsandretentionDurationInNswill silently fail even when valid numeric values are present in the JSON.JSONSerializationreturnsNSNumberfor numeric values, which doesn't bridge directly toInt64. This will causeDisappearingMessageSettingsto never be created even when the values are provided. Use(jsonOptions["disappearStartingAtNs"] as? NSNumber)?.int64Valueinstead. [ Already posted ]ios/Wrappers/EnrichedMessageQueryParamsWrapper.swift — 0 comments posted, 1 evaluated, 1 filtered
as? Int64forbeforeNs,afterNs,insertedAfterNs, andinsertedBeforeNswill silently fail and returnnileven when valid numeric values are present in the JSON.JSONSerializationreturnsNSNumberfor numeric values, which doesn't bridge directly toInt64. Use(jsonOptions["beforeNs"] as? NSNumber)?.int64Valuepattern instead. [ Already posted ]ios/Wrappers/MessageWrapper.swift — 1 comment posted, 6 evaluated, 4 filtered
encodeToObjV2is marked withthrowsbut never actually throws an error - all internal throwing calls usetry?to swallow errors. While not a runtime crash, this is misleading sinceencodeV2usestry encodeToObjV2(model)expecting potential errors that will never come from this method. [ Low confidence ]try?withcompactMapon line 33-34 silently drops any reactions that fail to encode. IfencodeToObjV2(reaction)throws for any reaction, it will be filtered out of the results without any error or warning. This causes silent data loss where the consumer receives an incompletereactionsarray and areactionCountthat doesn't match the number of reactions returned. [ Already posted ]ReactionV2Schema.fromString()toReactionSchema(rawValue:)may fail silently.ReactionV2Schema.fromStringhandles "unicode", "shortcode", "custom" with a fallback to.unknown, butReactionSchema(rawValue:)returnsnilfor unrecognized values. If the raw value strings differ between the V2 helper andReactionSchemaenum, the schema would benil. [ Already posted ]schemevalue betweenContentTypeRemoteAttachment(line 385:"https://") andContentTypeMultiRemoteAttachment(line 395:"https"). If downstream consumers expect a consistent format, this mismatch will cause parsing or URL reconstruction failures. [ Already posted ]src/lib/Conversations.ts — 0 comments posted, 1 evaluated, 1 filtered
streamMessageDeletionsmethod assigns the new subscription tothis.subscriptions[EventTypes.MessageDeletion]without removing any existing subscription at that key. IfstreamMessageDeletionsis called multiple times on the sameConversationsinstance (e.g., in a component lifecycle or multiple subscribers), the reference to the previous subscription is overwritten and lost. SincecancelStreamMessageDeletionsuses this key to find the subscription to remove, the overwritten subscription can never be cleaned up via that method. This results in a permanent memory leak of the event listener and duplicate execution of callbacks for incoming deletion events. [ Already posted ]src/lib/DecodedMessageV2.ts — 0 comments posted, 7 evaluated, 5 filtered
DecodedMessageV2.frommethod incorrectly propagates the parent message's genericContentTypeto nestedreactions. When a parent message (e.g.,TextCodec) is parsed, its reactions are instantiated asDecodedMessageV2<TextCodec>viafromObject<ContentType>. However, at runtime,reaction.content()decodes the payload using the reaction's actual codec (found viathis.contentTypeIdfrom the JSON), returning a reaction object. TypeScript expects a string (due to theTextCodecgeneric), leading to runtime crashes if string methods (liketoUpperCase) are called on the returned object. [ Already posted ]DecodedMessageV2.frommethod deserializes JSON directly into the class properties without converting date strings toDateobjects.JSON.parsereturns strings for date fields (likesentAtandexpiresAt), but the class properties are typed asDate. When a consumer attempts to callDatemethods (e.g.,msg.sentAt.getTime()) on these properties, the application will crash withTypeError: ... is not a functionbecause the runtime value is a string. [ Already posted ]DecodedMessageV2class fails to validate the existence ofnativeContenton the parsed JSON object before assigning it. If the native layer returns a partial or malformed JSON object wherenativeContentis missing (undefined), thecontent()method will throw aTypeError(e.g.,Cannot read properties of undefined (reading 'encoded')) when accessingthis.nativeContent.encoded. [ Already posted ]DecodedMessageV2.content()introduces a critical flaw in how decoders are selected for native content. The loop at lines 199-210 iterates over all registered codecs. The conditionallowEmptyProperties.some(...)(lines 202-204) checks ifnativeContenthas any allowed empty property (e.g.,'text') but fails to verify if that property corresponds to the currentcodecin the loop. [ Already posted ]DecodedMessageV2.content(), the logic iterates over all registered codecs inClient.codecRegistryto find a matchingNativeContentCodec. However, it accessescodec.contentKeywithout first verifying that the property exists on the codec object.Client.codecRegistrycan contain bothJSContentCodec(which does not havecontentKey) andNativeContentCodec(which does). Accessingcodec.contentKeyon aJSContentCodecwill returnundefined. Whilethis.nativeContent[undefined]is generally safe in JS (returns undefined), ifallowEmptyPropertieslogic is not met, the loop continues. If aJSContentCodecis encountered, the code does not crash immediately at the property check, but if the loop relies oncodecbeing aNativeContentCodecfor thedecodecall later, we must ensure it is one. More critically, the check('contentKey' in codec ...)is present, but TypeScript runtime behavior forinoperator on objects is safe. The issue here is subtle: ifthis.nativeContent.hasOwnProperty(prop)is true for an empty property (liketext), the code executes(codec as NativeContentCodec<...>).decode(this.nativeContent). If the currentcodecin the loop happens to be aJSContentCodec(which lacksdecode(NativeMessageContent)signature or implementation compatible with it, usually expectingEncodedContent), this will crash or throw a type error at runtime becauseJSContentCodec.decodetakesEncodedContent, notNativeMessageContent. The loop iteratesObject.values(Client.codecRegistry), so it mixes both types. [ Already posted ]src/lib/Dm.ts — 0 comments posted, 4 evaluated, 3 filtered
prepareMessagemethod acceptsSendOptions(which includesshouldPush), but fails to pass this parameter to the underlying native calls. Specifically, in_prepareWithJSCodec(line 171) and the directXMTP.prepareMessagecall (line 159), theshouldPushoption is dropped. This means users cannot control push notification behavior for prepared messages (e.g., suppressing push for silent messages), unlike thesendmethod which correctly processesopts.shouldPush. [ Already posted ]prepareMessagemethod acceptsopts: SendOptions(which includesshouldPush?: boolean), but the implementation discards theshouldPushproperty in both execution paths. When calling_prepareWithJSCodec(line 162),shouldPushis not passed, and_prepareWithJSCodecitself does not accept it as an argument. Similarly, in the direct native call path (line 170),XMTP.prepareMessageis called without any push configuration. BecauseprepareMessageperforms an optimistic send by default (whennoSendis false), users invokingprepareMessagewithshouldPush: falsewill have this preference ignored, resulting in unintended push notifications if the underlying default is to push. [ Already posted ]publishMessagemethod triggers the sending of a previously prepared message, but becauseprepareMessagedropped theshouldPushpreference during the initial storage phase,publishMessagehas no way to retrieve or respect the user's original intent regarding push notifications. This results in the message being published with default push behavior (typically enabled) regardless of the options provided during preparation. [ Already posted ]